fix(dns): Fix response buffer race condition in concurrent queries#1054
fix(dns): Fix response buffer race condition in concurrent queries#1054abhik-roy85 wants to merge 3 commits into
Conversation
7a1adb3 to
f2b75d9
Compare
f2b75d9 to
ec6a7b6
Compare
euripedesrocha
left a comment
There was a problem hiding this comment.
I did a partial review, but there are some discussions that might lead to structural changes.
| #define ESP_DNS_BUFFER_SIZE 512 | ||
| /** Maximum number of resource records to scan in a response */ | ||
| #ifndef ESP_DNS_MAX_ANSWER_SCAN | ||
| #define ESP_DNS_MAX_ANSWER_SCAN 16 |
There was a problem hiding this comment.
Should this be controlled by the user?
There was a problem hiding this comment.
Added CONFIG_ESP_DNS_MAX_ANSWER_SCAN under Component config → ESP DNS (default 16).
| err_t status; /* Status of the answer */ | ||
| ip_addr_t ip; /* IP address from the answer */ | ||
| } dns_answer_storage_t; | ||
| #define ESP_DNS_BUFFER_SIZE 512 |
There was a problem hiding this comment.
Could be configurable as well.
There was a problem hiding this comment.
Same pattern: CONFIG_ESP_DNS_BUFFER_SIZE in Kconfig (default 512 bytes).
| * @param response The DNS response to process. | ||
| * @param ipaddr An array to store the extracted IP addresses. | ||
| * @param response The parsed DNS response | ||
| * @param ipaddr Array to store the copied IP addresses |
There was a problem hiding this comment.
Isn't better to have user to pass max number of entries?
There was a problem hiding this comment.
dns_resolve_* now take addr_cnt; esp_dns_parse_response() and esp_dns_get_ips_from_response() take max_ips / max_entries, clamped via esp_dns_clamp_addr_cnt(). The lwIP hook still passes MAX_ANSWERS (1) until LWIP_HOOK_NETCONN_EXTERNAL_RESOLVE forwards the caller's buffer size.
| uint16_t id; /* Transaction ID */ | ||
| int num_answers; /* Number of valid answers */ | ||
| dns_answer_storage_t answers[MAX_ANSWERS]; /* Array of answers */ | ||
| ip_addr_t answers[MAX_ANSWERS]; /* Array of IP addresses */ |
There was a problem hiding this comment.
Is this a breaking change? I think we are under 1.0, but it is worth to guide users on this.
There was a problem hiding this comment.
dns_response_t and the parse/extract helpers live in esp_dns_utils.h, which is component-private (PRIV_INCLUDE_DIRS). The public API is only include/esp_dns.h (init/cleanup + config). App code integrates via getaddrinfo() after esp_dns_init_*(); those signatures are unchanged. So this isn't a user-facing breaking change — no migration guide needed.
| - **Certificate Errors**: | ||
| - Verify that the correct certificate is provided for secure protocols | ||
| - For public DNS servers, use the certificate bundle approach | ||
| - If you see `No matching trusted root certificate found` when using the certificate bundle (e.g. with `dns.google` or Cloudflare), the server likely uses a cross-signed chain. You must enable `CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_CROSS_SIGNED_VERIFY=y` in your `sdkconfig` (under Component config → mbedTLS → Certificate Bundle → Support cross-signed certificate verification). |
There was a problem hiding this comment.
How likely is this to happen? Should we have this set by default and let user to opt-out instead?
There was a problem hiding this comment.
Cross-signed chains are common with public DoT/DoH when using the cert bundle. esp_dns can't default mbedTLS Kconfig from the component, but the examples and concurrent test ship with CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_CROSS_SIGNED_VERIFY=y in sdkconfig.defaults. Updated the README to recommend the same for production projects and document how to opt out.
|
|
||
| /* ESP-IDF xTaskCreate() stack size is in bytes. DoT/DoH paths pull in mbedTLS inside | ||
| * getaddrinfo(); 4 KiB is too small and triggers a stack overflow on typical builds. */ | ||
| #define DNS_ADDRINFO_TASK_STACK 8192 |
There was a problem hiding this comment.
Make it configurable?
There was a problem hiding this comment.
Added CONFIG_EXAMPLE_DNS_ADDRINFO_TASK_STACK in the example's Kconfig.projbuild (default 8192 bytes). Tunable via menuconfig under the example options.
|
|
||
| * **Certificate Issues**: | ||
| For DoT and DoH protocols, ensure that the certificates are valid for the DNS server you're using. The example includes Google DNS certificates, but these may need to be updated if they expire. | ||
| If you are using the Certificate Bundle and see a `No matching trusted root certificate found` error, it is likely due to cross-signed chains (e.g., from Google). Make sure that `CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_CROSS_SIGNED_VERIFY=y` is enabled in your configuration. This can be enabled via `idf.py menuconfig` -> `Component config` -> `mbedTLS` -> `Certificate Bundle` -> `Support cross-signed certificate verification`. |
There was a problem hiding this comment.
Should it be set as default and let user opt out?
There was a problem hiding this comment.
Enabled by default in this example's sdkconfig.defaults. Updated the troubleshooting text to say that instead of asking users to turn it on manually.
| | Supported Targets | ESP32 | ESP32-C2 | ESP32-C3 | ESP32-C5 | ESP32-C6 | ESP32-C61 | ESP32-H2 | ESP32-P4 | ESP32-S2 | ESP32-S3 | | ||
| | ----------------- | ----- | -------- | -------- | -------- | -------- | --------- | -------- | -------- | -------- | -------- | | ||
|
|
||
| This example stress-tests **`getaddrinfo()`** while the **esp_dns** component owns the lwIP **external resolve** hook. It is meant to surface concurrency bugs documented in [ISSUES_AND_PROGRESS.md](../../ISSUES_AND_PROGRESS.md): |
There was a problem hiding this comment.
Remove the reference of non-committed file.
There was a problem hiding this comment.
Removed the ISSUES_AND_PROGRESS.md reference from the test README. That tracker is internal-only and is not part of the component.
| @@ -0,0 +1,49 @@ | |||
| # esp_dns concurrent resolver test | |||
There was a problem hiding this comment.
If this is a test app, should be treated as one. Not on the examples.
There was a problem hiding this comment.
Moved to tests/test_apps/esp_dns_concurrent and updated the README/CI accordingly.
|
|
||
| This example stress-tests **`getaddrinfo()`** while the **esp_dns** component owns the lwIP **external resolve** hook. It is meant to surface concurrency bugs documented in [ISSUES_AND_PROGRESS.md](../../ISSUES_AND_PROGRESS.md): | ||
|
|
||
| - **Issue 1** — shared singleton resolver state (`response_buffer`, transaction id) under concurrent lookups. |
There was a problem hiding this comment.
Should we review the architecture and adopt a queue to the requests? This way we would fix the issu by removing the singleton.
There was a problem hiding this comment.
Keeping the singleton for now. Per-query response_buffer on the stack (and DoH user_data) removes the shared mutable state that caused the races, without serializing lookups or adding a request queue. A full queue-based redesign would be a large API/architecture change, and involve a lot more analysis and testing. Will take it up for a future release.
ec6a7b6 to
135fa98
Compare
135fa98 to
a039ce1
Compare
a039ce1 to
3f7ca0f
Compare
Moved `response_buffer` out of the global handle context (`esp_dns_handle`) and into automatic/local storage within `dns_resolve_doh`, `dns_resolve_dot`, and `dns_resolve_tcp`. This prevents memory corruption and race conditions when multiple DNS queries are running concurrently. Also included `esp_dns_concurrent_test` example to validate concurrent lookup behavior.
Store ip_addr_t values directly in dns_response_t, rename esp_dns_extract_ip_addresses_from_response to esp_dns_get_ips_from_response, and scan up to CONFIG_ESP_DNS_MAX_ANSWER_SCAN RRs while collecting addresses. Add addr_cnt to dns_resolve_* and max_ips/max_entries to the parse/extract helpers. Add CONFIG_ESP_DNS_MAX_ANSWER_SCAN and CONFIG_ESP_DNS_BUFFER_SIZE.
Migrate esp_dns_basic to net_connect, make the getaddrinfo worker stack configurable, and enable cross-signed certificate verification in sdkconfig.defaults. Move the concurrent stress app to tests/test_apps/esp_dns_concurrent and add it to esp_dns CI builds.
3f7ca0f to
6b66578
Compare
Description
Fixes a race condition when multiple
getaddrinfo()calls run concurrently through the esp_dns lwIP hook (DoT/TCP/DoH). Also addresses review feedback on configurability, examples, and test layout.Component changes
response_buffer_tto stack-local storage indns_resolve_doh,dns_resolve_dot, anddns_resolve_tcp. DoH passes the per-query buffer viaesp_http_client_config_t::user_dataso overlapping HTTP events do not share state.CONFIG_ESP_DNS_MAX_ANSWER_SCANRRs while collecting addresses, so CNAME + A/AAAA in one response works whenMAX_ANSWERSis 1.ip_addr_tdirectly indns_response_t; renameesp_dns_extract_ip_addresses_from_response→esp_dns_get_ips_from_response.dns_resolve_*takeaddr_cnt;esp_dns_parse_response()andesp_dns_get_ips_from_response()takemax_ips/max_entries, clamped viaesp_dns_clamp_addr_cnt(). The lwIP hook passesMAX_ANSWERS(1) untilLWIP_HOOK_NETCONN_EXTERNAL_RESOLVEforwards the caller'saddr_cnt.CONFIG_ESP_DNS_MAX_ANSWER_SCANandCONFIG_ESP_DNS_BUFFER_SIZEunder Component config → ESP DNS.The public API (
include/esp_dns.h) is unchanged. Parse/extract helpers remain component-private.Examples and tests
esp_dns_basicand the concurrent stress app fromprotocol_examples_commontonet_connect(separate commit from component parsing changes).CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_CROSS_SIGNED_VERIFYin example/testsdkconfig.defaults; document opt-out in READMEs.esp_dns_basicgetaddrinfoworker stack to 8192 bytes by default; make it configurable viaCONFIG_EXAMPLE_DNS_ADDRINFO_TASK_STACK.examples/esp_dns_concurrent_testtotests/test_apps/esp_dns_concurrent.esp_dns__build.ymlCI to build bothesp_dns_basicandesp_dns_concurrent.Related
Testing
Checklist
Before submitting a Pull Request, please ensure the following:
Note
Medium Risk
Touches core DNS resolution paths used by
getaddrinfo()and TLS/HTTP clients; behavior changes are intentional but concurrent networking code warrants careful review.Overview
Fixes a race when multiple
getaddrinfo()calls run through the esp_dns lwIP hook for TCP/DoT/DoH by removing sharedresponse_buffer_tfrom the DNS handle and keeping per-lookup state on the stack. DoH wires that buffer throughesp_http_clientuser_dataso overlapping HTTP callbacks do not clobber each other.Parsing and safety:
dns_resolve_*now takeaddr_cnt(clamped viaesp_dns_clamp_addr_cnt()); the lwIP hook still passesMAX_ANSWERS(1) until lwIP forwards caller buffer size. Answer parsing can scan up toCONFIG_ESP_DNS_MAX_ANSWER_SCANRRs while collecting IPs (helps CNAME chains); buffer size isCONFIG_ESP_DNS_BUFFER_SIZE.esp_dns_extract_ip_addresses_from_responseis renamed toesp_dns_get_ips_from_response.Examples, tests, CI:
esp_dns_basicand a newesp_dns_concurrentstress app usenet_connectinstead ofprotocol_examples_common; cross-signed cert bundle verification and larger defaultgetaddrinfoworker stack are documented and defaulted. CI builds both apps on updated IDF matrix versions.Reviewed by Cursor Bugbot for commit 6b66578. Bugbot is set up for automated code reviews on this repo. Configure here.